package config

import (
	"container/list"
	"errors"
	"fmt"
	"github.com/spf13/cast"
	"github.com/spf13/viper"
	"os"
	"path/filepath"
	"strings"
	"sync"
)

const keyDelim string = "."

type vpNode struct {
	filepath string
	vp       *viper.Viper
}

// VanguradConfig config for Rock framework
type VanguradConfig struct {
	vpTree      map[string]interface{}  // vpNode Tree corresponding to the config directory tree
	mu          sync.Mutex              // mutex for config changing
	watches     map[string]*list.List   // watch funcs map
	watchesMu   sync.Mutex              // mutex for adding watch functions
	watchingVps map[string]*viper.Viper // map from vpNode path in the tree to the viper struct which watches the config file
}

// WatchFunc is the callback function when the watched configure changed
type WatchFunc func(key string, oldVal interface{}, newVal interface{})

var GlobalConfig *VanguradConfig
var globalCfgMu sync.Mutex

// init global config
func InitGlobalConfig(path *string) {
	Get(*path)
	GlobalConfig.Unmarshal(&Conf)
}

// addVpNode add a vpNode into the vpTree at the specified path
func (rc *VanguradConfig) addVpNode(key string, vpn *vpNode) {
	path := strings.Split(key, keyDelim)
	node := rc.vpTree
	for _, k := range path[0 : len(path)-1] {
		nextNode, ok := node[k]
		if !ok {
			node[k] = make(map[string]interface{})
			nextNode = node[k]
		}
		switch nextNode.(type) {
		case map[string]interface{}:
			node = nextNode.(map[string]interface{})
		default:
			panic("Viper tree conflict!")
		}
	}
	node[path[len(path)-1]] = vpn
}

// walkVpTree walk through the vpTree according to the key. Returns the vpNode path and vpNode, or the key and the subTree
func (rc *VanguradConfig) walkVpTree(key string) (string, interface{}) {
	path := strings.Split(key, keyDelim)
	if rc.vpTree[""] != nil {
		return "", rc.vpTree[""]
	}
	node := rc.vpTree
	for i, k := range path {
		if node[k] == nil {
			return "", nil
		}
		nextNode := node[k]
		switch nextNode.(type) {
		case map[string]interface{}:
			node = nextNode.(map[string]interface{})
		case *vpNode:
			return strings.Join(path[0:i+1], keyDelim), nextNode
		default:
			panic("Unexpected type in vpTree")
		}
	}
	return key, node
}

// Get returns the global singleton config
// You can configure by `config.yaml` file in the working directory, or you can also configure by a `config` folder with yaml files in it.
// The configure section `cfg1` in the config/app1.yaml file can be get by rc.Get("app1.cfg1")
func Get(configPath string) {
	globalCfgMu.Lock()
	defer globalCfgMu.Unlock()
	if GlobalConfig == nil {
		if _, err := os.Stat(configPath); err != nil {
			path, _ := os.Getwd()
			configPath = path + "/config.yaml"
			if _, err := os.Stat(configPath); os.IsNotExist(err) {
				configPath = path + "/config"
				if _, err := os.Stat(configPath); os.IsNotExist(err) {
					panic("No `config.yaml` or `config` folder found in current working directory: " + path)
				}
			}
		}
		if vc, err := New(configPath); err != nil {
			panic("Error init config: " + err.Error())
		} else {
			GlobalConfig = vc
		}
	}
}

// New creates a new VanguradConfig
// You can pass in either a yaml filepath, or a directory as configPath
func New(configPath string) (*VanguradConfig, error) {
	rc := VanguradConfig{vpTree: make(map[string]interface{})}
	fi, err := os.Stat(configPath)
	if err != nil {
		return nil, err
	}
	mode := fi.Mode()
	if mode.IsDir() {
		err = filepath.Walk(configPath, func(path string, info os.FileInfo, err error) error {
			if err != nil {
				return err
			}
			if strings.HasSuffix(path, ".yaml") {
				vp := viper.New()
				vp.SetConfigFile(path)
				err := vp.ReadInConfig()
				if err != nil {
					return err
				}
				rootName := path[len(configPath) : len(path)-5]
				if rootName[0] == '/' {
					rootName = rootName[1:]
				}
				rootName = strings.Replace(rootName, "/", keyDelim, -1)
				rc.addVpNode(rootName, &vpNode{path, vp})
			} else if info.IsDir() {
				if _, err := os.Stat(path + ".yaml"); err == nil {
					// dir.yaml exists, ignore dir
					fmt.Printf("Ignore config directory: %s because the config file: %s.yaml exists\n", path, path)
					return filepath.SkipDir
				}
			}
			return err
		})
		if err != nil {
			return nil, err
		}
	} else {
		vp := viper.New()
		vp.SetConfigFile(configPath)
		vp.ReadInConfig()
		rc.addVpNode("", &vpNode{configPath, vp})
	}
	return &rc, nil
}

// Sub get sub section of the config
func (rc *VanguradConfig) Sub(key string) *VanguradConfig {
	rc.mu.Lock()
	defer rc.mu.Unlock()
	root, vpn := rc.walkVpTree(key)
	if vpn == nil {
		return nil
	}
	if root == key {
		switch vpn.(type) {
		case map[string]interface{}:
			return &VanguradConfig{vpTree: vpn.(map[string]interface{})}
		case *vpNode:
			subVpTree := make(map[string]interface{})
			subVpTree[""] = vpn
			return &VanguradConfig{vpTree: subVpTree}
		default:
			return nil
		}
	} else {
		var sub *viper.Viper
		if root == "" {
			sub = vpn.(*vpNode).vp.Sub(key)
		} else {
			sub = vpn.(*vpNode).vp.Sub(key[len(root)+1:])
		}
		if sub == nil {
			return nil
		}
		subRc := &VanguradConfig{vpTree: make(map[string]interface{})}
		subRc.addVpNode("", &vpNode{vp: sub})
		return subRc
	}
}

// Get returns the value associated the key, basic type value or map[string]interface{} is returned
func (rc *VanguradConfig) Get(key string) interface{} {
	rc.mu.Lock()
	defer rc.mu.Unlock()
	root, vpn := rc.walkVpTree(key)
	if vpn == nil {
		return nil
	}
	if root == key {
		switch vpn.(type) {
		case map[string]interface{}:
			return vpn.(map[string]interface{})
		case *vpNode:
			return vpn.(*vpNode).vp.AllSettings()
		default:
			return nil
		}
	} else {
		if root == "" {
			return vpn.(*vpNode).vp.Get(key)
		}
		return vpn.(*vpNode).vp.Get(key[len(root)+1:])
	}
}

// Unmarshal the specified config sub section associated with the key into the given struct val
func (rc *VanguradConfig) UnmarshalSub(key string, val interface{}) error {
	sub := rc.Sub(key)
	if sub == nil {
		return errors.New("No such config section: " + key)
	}
	if sub.vpTree[""] == nil {
		panic("Unmarshal across multiple config files is not supported")
	}
	return sub.vpTree[""].(*vpNode).vp.Unmarshal(val)
}

// GetBool returns config associated with the key as bool
func (rc *VanguradConfig) GetBool(key string) bool {
	return cast.ToBool(rc.Get(key))
}

// GetString returns config associated with the key as string
func (rc *VanguradConfig) GetString(key string) string {
	return cast.ToString(rc.Get(key))
}

// GetInt returns the config associated with the key as int
func (rc *VanguradConfig) GetInt(key string) int {
	return cast.ToInt(rc.Get(key))
}

// GetFloat32 returns the config associated with the key as float32
func (rc *VanguradConfig) GetFloat32(key string) float32 {
	return cast.ToFloat32(rc.Get(key))
}

// GetFloat64 returns the config associated with the key as float64
func (rc *VanguradConfig) GetFloat64(key string) float64 {
	return cast.ToFloat64(rc.Get(key))
}

func (rc *VanguradConfig) Unmarshal(val interface{}) {
	if err := rc.vpTree[""].(*vpNode).vp.Unmarshal(val); err != nil {
		_ = fmt.Errorf("loads error")
		os.Exit(1)
	}
}
